Skip to content

Feature: add a new event for many-to-many relationships#498

Open
andrestejerina97 wants to merge 3 commits intomainfrom
feature/new-event-for-formatters
Open

Feature: add a new event for many-to-many relationships#498
andrestejerina97 wants to merge 3 commits intomainfrom
feature/new-event-for-formatters

Conversation

@andrestejerina97
Copy link
Contributor

@andrestejerina97 andrestejerina97 commented Feb 11, 2026

ref: https://app.clickup.com/t/86b84w9x2

Summary by CodeRabbit

  • New Features

    • Audit logging now reports many-to-many collection updates and deletions with detailed, human-readable summaries, counts and optional child-entity formatting; new formatters added. PresentationCategoryGroup auditing enabled in config.
  • Improvements

    • Listener and export logic extended to recognize and route many-to-many collection events; new audit metadata extraction and safer message builders.
  • Bug Fixes

    • Improved robustness, error handling and logging when extracting collection metadata and building messages.
  • Tests

    • Extensive new tests and helpers covering many-to-many collection auditing and formatters.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 2 times, most recently from ffb0da8 to e142976 Compare February 13, 2026 19:22
@andrestejerina97 andrestejerina97 marked this pull request as ready for review February 18, 2026 14:02
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 3 times, most recently from 1613295 to bd0d027 Compare February 18, 2026 18:02
@andrestejerina97
Copy link
Contributor Author

@martinquiroga-exo ready to review

Copy link
Contributor

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

];

$eventType = $isManyToMany
? ($isDeletion ? IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE : IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this concatenated ternary operators, perhaps I would like to see an exit early guard clause when the event is an EVENT_COLLECTION_UPDATE.

I defer to Casey on this one since is a design issue rather than an implementation one.

Copy link

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to defer to whatever Seb comes up with but this is what I'm seeing.

The auditCollection() method changed the calling convention for all collection updates not just ManyToMany. For non-ManyToMany collections, it now passes $owner (the entity) as the subject instead of $col (the collection itself), and $payload instead of [] as the change set.

Both strategies downstream expect a PersistentCollection as $subject for EVENT_COLLECTION_UPDATE:

  • OTLP strategy: instanceof PersistentCollection checks fail, formatter returns null, audit entries silently dropped
  • DB strategy: calls count($subject) and $subject->getSnapshot() on the entity — throws \Error (not \Exception), uncaught by the catch block

Fix: Unless the intent was to actually change the behavior for all for some reason, only change the subject/payload for ManyToMany events; preserve the original $strategy->audit($col, [], EVENT_COLLECTION_UPDATE, $ctx) call for non-ManyToMany collections.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 3 times, most recently from 090b9c5 to 186913f Compare February 26, 2026 19:06
@andrestejerina97
Copy link
Contributor Author

@caseylocker it's ready to review

Copy link

@caseylocker caseylocker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two fixes needed before merge:

  1. app/Audit/AuditLogOtlpStrategy.php — Add M2M event mappings

mapEventTypeToAction() and getLogMessage() need explicit cases for EVENT_COLLECTION_MANYTOMANY_UPDATE (→ ACTION_COLLECTION_UPDATE / LOG_MESSAGE_COLLECTION_UPDATED) and EVENT_COLLECTION_MANYTOMANY_DELETE (→ ACTION_DELETE / LOG_MESSAGE_DELETED). Without this, M2M audit entries hit Elasticsearch with action=unknown, making them invisible to dashboards/queries filtering on action type.

buildAuditLogData() should also extract collection metadata from $change_set['collection'] for M2M events, since the subject is the owner entity (not a PersistentCollection).

  1. app/Audit/AuditLogFormatterFactory.php — Fix generic M2M fallback

Lines 110-123: when no context-specific formatter exists, the fallback creates EntityCollectionUpdateAuditLogFormatter and passes the owner entity as $subject. That formatter calls $subject->getInsertDiff() (line 50 of EntityCollectionUpdateAuditLogFormatter), which doesn't exist on the owner entity. This either silently drops the audit entry or throws an uncaught \Error. The fallback should pass $change_set['collection'] as the subject instead of the owner, or use a M2M-aware generic formatter.

Why this blocks merge: Without fix 1, OTLP audit data is misclassified. Without fix 2, any M2M collection change on an entity without a registered custom formatter will silently fail or crash.

@matiasperrone-exo matiasperrone-exo force-pushed the feature/new-event-for-formatters branch from 186913f to 8af3df1 Compare March 3, 2026 18:54
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch 2 times, most recently from 02a1841 to be8a5f1 Compare March 5, 2026 17:21
@smarcet
Copy link
Collaborator

smarcet commented Mar 5, 2026

@andrestejerina97 please review unit tests https://github.com/OpenStackweb/summit-api/actions/runs/22728527050/job/65910842285?pr=498#step:5:68

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from be8a5f1 to 6b78204 Compare March 5, 2026 18:55
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from 6b78204 to 9d27bf5 Compare March 5, 2026 18:55
@andrestejerina97
Copy link
Contributor Author

@caseylocker it's ready to review

@smarcet smarcet force-pushed the feature/new-event-for-formatters branch from 1d0e92b to b3d0359 Compare March 6, 2026 13:02
@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from b3d0359 to 1e069fd Compare March 11, 2026 14:20
@coderabbitai
Copy link

coderabbitai bot commented Mar 11, 2026

📝 Walkthrough

Walkthrough

Adds end-to-end support for auditing many-to-many collection updates and deletions: new event/action constants, collection metadata value object, listener collection handling and query paths, formatter changes and new concrete formatters, strategy and factory updates, config entry, and extensive unit tests and helpers.

Changes

Cohort / File(s) Summary
Core Audit Types
app/Audit/Interfaces/IAuditStrategy.php
Added four public constants for many-to-many collection events/actions.
PersistentCollection Metadata
app/Audit/PersistentCollectionMetadata.php
New value object encapsulating PersistentCollection mapping, target entity, initialization state, preloaded deleted IDs, and collection reference.
Base Formatter Enhancements
app/Audit/AbstractAuditLogFormatter.php
Imported PersistentCollection and Log; added helpers: handleManyToManyCollection, processCollection, extractCollectionEntityIds, buildManyToManyDetailedMessage, formatManyToManyDetailedMessage; integrated M:N formatting and safe logging.
New Concrete Formatters
app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php, app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
Added two formatters for many-to-many update/delete events; support optional child-entity formatter delegation and detailed or child-level messages with error logging.
Event Listener & Collection Handling
app/Audit/AuditEventListener.php
Stored EntityManager as $em; added auditCollection and fetchManyToManyIds helpers; switched collection auditing to helper paths and added logging/error handling for M:N collections.
Strategy & Factory
app/Audit/AuditLogFormatterFactory.php, app/Audit/AuditLogOtlpStrategy.php
Factory resolves new M:N event formatters; OTLP strategy handles MANYTOMANY update/delete events, derives collection type/counts/dirty flags, and maps events to actions/messages.
Existing Formatter Extensions
app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php, app/Audit/ConcreteFormatters/PresentationCategoryGroupAuditLogFormatter.php
Added branches and private handlers for M:N update/delete events; added M:N-specific formatting helpers and minor message adjustments.
Config
config/audit_log.php
Added PresentationCategoryGroup mapping to audit log config.
Test Helpers
tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
New test helper to build PersistentCollection instances (snapshot vs current) for many-to-many tests.
Tests - Formatters & Listener
tests/OpenTelemetry/Formatters/*, tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
Added comprehensive unit/integration tests for listener collection logic, new formatters, SummitAttendee and PresentationCategoryGroup M:N handling, and updated all-formatters integration.
Misc Tests
tests/SummitRSVPServiceTest.php
Replaced @covers docblock with PHPUnit CoversClass attribute.

Sequence Diagram

sequenceDiagram
    actor Client
    participant UOW as UnitOfWork
    participant Listener as AuditEventListener
    participant EM as EntityManager
    participant Strategy as AuditLogOtlpStrategy
    participant Factory as AuditLogFormatterFactory
    participant Formatter as ManyToManyFormatter

    Client->>UOW: trigger flush
    UOW->>Listener: onFlush()
    Listener->>EM: use stored EntityManager ($em)
    Listener->>Listener: auditCollection(subject,uow,eventType)
    Listener->>EM: fetchManyToManyIds if needed (join table)
    Listener->>Strategy: record event with payload (counts, deleted_ids)
    Strategy->>Factory: resolve formatter for event
    Factory->>Formatter: provide formatter
    Formatter->>Formatter: handleManyToManyCollection/processCollection
    Formatter-->>Strategy: formatted message
    Strategy->>Client: persist/emit audit record
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I hopped through relations, sniffed each change and thread,

Found added carrots, pulled the old ones dead,
Listeners kept watch, formatters hummed along,
M:Ns now sang their tidy audit song,
A rabbit's tiny trumpet: logs all neat and sped.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 5.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature: add a new event for many-to-many relationships' clearly describes the main change: introducing new audit events for many-to-many collection changes. It directly corresponds to the PR's primary objective of adding EVENT_COLLECTION_MANYTOMANY_UPDATE and EVENT_COLLECTION_MANYTOMANY_DELETE events with supporting formatters and handlers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/new-event-for-formatters
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

CodeRabbit can use your project's PHP CodeSniffer (phpcs) configuration to improve the quality of PHP code reviews.

Add a phpcs.xml or phpcs.xml.dist file to your project to customize how CodeRabbit runs phpcs. See PHP CodeSniffer documentation for more details.

@andrestejerina97
Copy link
Contributor Author

@martinquiroga-exo ready to review

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php (1)

147-169: ⚠️ Potential issue | 🟠 Major

Fail on unsupported event types.

$unsupported is populated but never asserted. Any formatter that still rejects EVENT_COLLECTION_MANYTOMANY_UPDATE or EVENT_COLLECTION_MANYTOMANY_DELETE through the "event type" path will keep this test green, so the new coverage can miss the PR's main behavior.

🛠️ Proposed fix
-        $this->assertEmpty($errors, "Event type handling failed:\n" . implode("\n", $errors));
+        $this->assertEmpty($unsupported, "Unsupported event types:\n" . implode("\n", $unsupported));
+        $this->assertEmpty($errors, "Event type handling failed:\n" . implode("\n", $errors));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php` around lines
147 - 169, The test collects unsupported formatters in $unsupported but never
fails on them, allowing regressions; after the loop and before asserting
$errors, add an assertion that $unsupported is empty (with a clear message) so
any formatterClass from discoverFormatters() that rejects event types like
EVENT_COLLECTION_MANYTOMANY_UPDATE or EVENT_COLLECTION_MANYTOMANY_DELETE (caught
via the "event type" path in the catch block) will fail the test; locate the
loop using FormatterTestHelper::assertFormatterCanBeInstantiated and the
$unsupported array and assertEmpty($unsupported, "Unsupported event types
found:\n" . implode("\n", $unsupported)).
🧹 Nitpick comments (7)
app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php (2)

118-123: Remove extraneous blank lines.

Lines 119-121 contain unnecessary whitespace that appears to be a leftover from editing.

🧹 Proposed fix
         try {
             $removed_ids = $collectionData['removed_ids'] ?? [];
-
-            
-
             $field = $collectionData['field'] ?? 'unknown';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php` around
lines 118 - 123, Remove the extraneous blank lines between the assignment of
$removed_ids and the subsequent assignments to $field and $targetEntity in the
SummitAttendeeAuditLogFormatter class so the code reads compactly as consecutive
assignments (ensure $removed_ids = $collectionData['removed_ids'] ?? []; is
immediately followed by $field = $collectionData['field'] ?? 'unknown'; and
$targetEntity = $collectionData['target_entity'] ?? 'unknown';).

59-76: Remove unused $subject parameter.

The $subject parameter is declared but never used within handleManyToManyCollection. Since $id and $name are already extracted and passed separately, this parameter is redundant.

♻️ Proposed fix
-    private function handleManyToManyCollection(SummitAttendee $subject, array $change_set, $id, $name, bool $isDeletion): ?string
+    private function handleManyToManyCollection(array $change_set, $id, $name, bool $isDeletion): ?string
     {

Also update the call sites in lines 47 and 50:

                 case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE:
-                    return $this->handleManyToManyCollection($subject, $change_set, $id, $name, false);
+                    return $this->handleManyToManyCollection($change_set, $id, $name, false);

                 case IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE:
-                    return $this->handleManyToManyCollection($subject, $change_set, $id, $name, true);
+                    return $this->handleManyToManyCollection($change_set, $id, $name, true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php` around
lines 59 - 76, The method handleManyToManyCollection has an unused $subject
parameter; remove $subject from its signature (change function
handleManyToManyCollection(SummitAttendee $subject, array $change_set, $id,
$name, bool $isDeletion): ?string to handleManyToManyCollection(array
$change_set, $id, $name, bool $isDeletion): ?string) and update all call sites
that pass the subject (the two places invoking handleManyToManyCollection) to
stop providing the extra argument so they pass only $change_set, $id, $name,
$isDeletion; ensure type hints and usages inside handleManyToManyCollection
remain consistent after removal.
app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php (1)

55-56: Consider: Inconsistent key naming between added_ids/removed_ids and deleted_ids.

The code checks for both 'removed_ids' and 'deleted_ids' as fallback. While this provides flexibility, it may lead to confusion about which key should be used. Consider documenting the expected payload structure or standardizing on one key name.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php`
around lines 55 - 56, Standardize the change_set keys by treating "removed_ids"
as the canonical name and explicitly map any legacy "deleted_ids" to it before
use in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter: in the code
that prepares $change_set (or immediately before computing
$addedIds/$removedIds) normalize $change_set['removed_ids'] =
$change_set['removed_ids'] ?? $change_set['deleted_ids'] ?? null, then set
$addedIds = $change_set['added_ids'] ?? null and $removedIds =
$change_set['removed_ids']; also update any docblocks/comments or
function/method docs that reference $change_set to state that "removed_ids" is
the standard key (and "deleted_ids" is supported as an alias).
app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php (1)

60-65: Debug logging may be verbose in production.

The debug logging includes the full deletedIds array content. For collections with many IDs, this could produce very large log entries. Consider logging only the count in debug mode.

🔧 Suggested improvement
             $deletedIds = isset($change_set['deleted_ids']) ? $change_set['deleted_ids'] : null;
             Log::debug("DefaultEntityManyToManyCollectionDeleteAuditLogFormatter::format - deletedIds", [
-                'deletedIds' => $deletedIds,
                 'isNull' => $deletedIds === null,
                 'count' => $deletedIds ? count($deletedIds) : 0
             ]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php`
around lines 60 - 65, The debug statement in
DefaultEntityManyToManyCollectionDeleteAuditLogFormatter::format currently logs
the entire $deletedIds array which can be huge; change the Log::debug call to
avoid serializing the full array and instead log only the count (use
count($deletedIds) or 0 when null) and a short flag indicating null/empty,
referencing the $deletedIds variable so the formatter still provides useful
diagnostic info without dumping the full collection.
app/Audit/AbstractAuditLogFormatter.php (1)

189-239: Edge case: Empty message detail when no IDs present.

When both $addedIds and $removedIds are empty arrays, formatManyToManyDetailedMessage will produce a message ending with a colon and empty detail: "Many-to-Many collection 'field' action: ". Consider handling this edge case.

🔧 Suggested improvement
     protected static function formatManyToManyDetailedMessage(array $details, int $addCount, int $removeCount, string $action): string
     {
         $field = $details['field'] ?? 'unknown';
         $target = $details['target_entity'] ?? 'unknown';
         $addedIds = $details['added_ids'] ?? [];
         $removedIds = $details['removed_ids'] ?? [];

         $parts = [];
         if (!empty($addedIds)) {
             $parts[] = sprintf("Added %d %s(s): %s", $addCount, $target, implode(', ', $addedIds));
         }
         if (!empty($removedIds)) {
             $parts[] = sprintf("Removed %d %s(s): %s", $removeCount, $target, implode(', ', $removedIds));
         }

         $detailStr = implode(' | ', $parts);
+        if (empty($detailStr)) {
+            return sprintf("Many-to-Many collection '%s' %s: no changes", $field, $action);
+        }
         return sprintf("Many-to-Many collection '%s' %s: %s", $field, $action, $detailStr);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AbstractAuditLogFormatter.php` around lines 189 - 239,
formatManyToManyDetailedMessage can produce a trailing colon with empty details
when both added and removed ID lists are empty; change the method to detect when
$parts is empty and return a cleaner message (e.g., "Many-to-Many collection
'FIELD' ACTION: no changes" or "Many-to-Many collection 'FIELD' ACTION" without
the colon). Update the logic in formatManyToManyDetailedMessage to check if
$parts is empty and set $detailStr to a sensible placeholder (or omit the colon)
before building the final sprintf; reference the method name
formatManyToManyDetailedMessage and the keys 'field', 'added_ids', 'removed_ids'
to locate the code to modify. Ensure unit/output formatting remains consistent
with existing messages.
app/Audit/AuditLogOtlpStrategy.php (1)

200-209: Minor: Inconsistent error message format.

The error message at line 209 has an unusual format with "AuditLogOtlpStrategy:: unknown targetEntity" (double colon followed by space). Consider using a consistent format.

🔧 Suggested fix
         } catch (\Exception $ex) {
-            return 'AuditLogOtlpStrategy:: unknown targetEntity';
+            return 'unknown';
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 200 - 209, The catch block
in AuditLogOtlpStrategy that currently returns "AuditLogOtlpStrategy:: unknown
targetEntity" should be changed to a consistently formatted message (for example
"AuditLogOtlpStrategy: unknown targetEntity") and preferably include the
exception details; update the return in the catch of the method that calls
$collection->getMapping() to return a string like "AuditLogOtlpStrategy: unknown
targetEntity - " . $ex->getMessage() (or at minimum remove the double colon and
extra space so it reads "AuditLogOtlpStrategy: unknown targetEntity").
tests/OpenTelemetry/Formatters/AuditEventListenerTest.php (1)

66-122: Add listener coverage for initialized collection deletions.

AuditEventListener::auditCollection() now has a separate delete path for initialized many-to-many collections via fetchDeletedIdsFromInitializedCollection(), but this file only exercises the non-many-to-many branch and the raw ID fetch helper. That leaves the new diff-based deleted_ids payload untested at the listener level. PersistentCollectionTestHelper should make this path straightforward to cover.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/OpenTelemetry/Formatters/AuditEventListenerTest.php` around lines 66 -
122, Add a new unit test that exercises AuditEventListener::auditCollection()'s
many-to-many delete branch by constructing an initialized PersistentCollection
(use PersistentCollectionTestHelper to create a collection that is initialized
and has removed elements), attach it to a mock owner and mapping for a
ManyToManyOwningSideMapping, inject a mocked EntityManager (and ClassMetadata)
into the AuditEventListener instance, then call auditCollection() and assert the
emitted payload includes the diff-style "deleted_ids" (e.g., [10,11]) produced
by AuditEventListener::fetchDeletedIdsFromInitializedCollection(); ensure the
test uses the same mocking pattern as existing tests (QueryBuilder/Connection
mocks if needed) and targets the listener-level behavior rather than the helper
method directly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Audit/AuditEventListener.php`:
- Around line 216-285: Remove the unused $uow parameter from the
fetchManyToManyIds method signature and any related docblock, then update all
call sites that pass $uow into fetchManyToManyIds to call it without that
argument; specifically edit the AuditEventListener::fetchManyToManyIds
declaration to drop $uow and modify the internal callers within the class to
call fetchManyToManyIds($collection) instead of passing the unused $uow. Ensure
nothing else relies on the removed parameter and run static analysis/tests to
confirm no remaining references.
- Line 114: The $ui array is initialized but never populated, making later
accesses to $ui['app'] and $ui['flow'] always null; update the
AuditEventListener code to either remove the unused $ui initialization and use
null/defaults directly where $ui['app']/$ui['flow'] are read, or populate $ui
from the current request/route context (e.g. pull values from the Request or
route attributes) before those reads so $ui['app'] and $ui['flow'] contain the
intended values; ensure you update the same method in AuditEventListener where
$ui is declared and where $ui['app']/$ui['flow'] are accessed.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php`:
- Around line 96-109: The child-entity creation loop is pushing potentially null
returns from $this->child_entity_formatter->format(...) into $changes, causing
null entries in the final string; update the creation loop (the foreach over
$insertDiff calling format with
IChildEntityAuditLogFormatter::CHILD_ENTITY_CREATION) to only append non-null
results (e.g., check the formatter return is not null before $changes[] = ...)
or run an array_filter on $changes before the implode, mirroring the
null-filtering behavior used for deletions (format(..., CHILD_ENTITY_DELETION)).

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php`:
- Around line 109-112: The formatters in SummitAttendeeAuditLogFormatter are
inconsistent: formatManyToManyUpdate catches exceptions and returns a fallback
message, while formatManyToManyDelete returns null; change
formatManyToManyDelete to mirror formatManyToManyUpdate by catching \Throwable
and returning the same formatted fallback string (e.g., using $id, $name and
$this->getUserInfo()) and logging the exception with Log::warning so both update
and delete produce a consistent audit fallback on error.

In
`@tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php`:
- Around line 250-264: The current formatter preserves an empty segment when a
child formatter returns null, causing a leading '|' in
DefaultEntityManyToManyCollectionUpdateAuditLogFormatter output; update the
format method in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter so it
collects child outputs from the IChildEntityAuditLogFormatter->format calls,
filters out null/empty values, and then joins the remaining pieces with the '|'
separator (only inserting separators between non-empty segments) so null child
results are skipped and no leading separator is emitted.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`:
- Around line 70-79: The test ManyToMany null-case provider currently always
calls format($attendee, []) so handleManyToManyCollection() exits on missing
collection and the DP_*_WITHOUT_CONTEXT rows never exercise the "collection
present but context missing" branch; update the DataProvider
'providesNullCasesForManyToMany' rows labeled DP_*_WITHOUT_CONTEXT to pass an
input array that includes a valid collection key (e.g. ['collection' => <dummy
collection value>]) but omits the required context key so
testManyToManyReturnsNullWithoutRequiredContextOrCollection (and the duplicate
cases around lines 203-210) will hit the no-context path in
handleManyToManyCollection().

---

Outside diff comments:
In `@tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php`:
- Around line 147-169: The test collects unsupported formatters in $unsupported
but never fails on them, allowing regressions; after the loop and before
asserting $errors, add an assertion that $unsupported is empty (with a clear
message) so any formatterClass from discoverFormatters() that rejects event
types like EVENT_COLLECTION_MANYTOMANY_UPDATE or
EVENT_COLLECTION_MANYTOMANY_DELETE (caught via the "event type" path in the
catch block) will fail the test; locate the loop using
FormatterTestHelper::assertFormatterCanBeInstantiated and the $unsupported array
and assertEmpty($unsupported, "Unsupported event types found:\n" . implode("\n",
$unsupported)).

---

Nitpick comments:
In `@app/Audit/AbstractAuditLogFormatter.php`:
- Around line 189-239: formatManyToManyDetailedMessage can produce a trailing
colon with empty details when both added and removed ID lists are empty; change
the method to detect when $parts is empty and return a cleaner message (e.g.,
"Many-to-Many collection 'FIELD' ACTION: no changes" or "Many-to-Many collection
'FIELD' ACTION" without the colon). Update the logic in
formatManyToManyDetailedMessage to check if $parts is empty and set $detailStr
to a sensible placeholder (or omit the colon) before building the final sprintf;
reference the method name formatManyToManyDetailedMessage and the keys 'field',
'added_ids', 'removed_ids' to locate the code to modify. Ensure unit/output
formatting remains consistent with existing messages.

In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 200-209: The catch block in AuditLogOtlpStrategy that currently
returns "AuditLogOtlpStrategy:: unknown targetEntity" should be changed to a
consistently formatted message (for example "AuditLogOtlpStrategy: unknown
targetEntity") and preferably include the exception details; update the return
in the catch of the method that calls $collection->getMapping() to return a
string like "AuditLogOtlpStrategy: unknown targetEntity - " . $ex->getMessage()
(or at minimum remove the double colon and extra space so it reads
"AuditLogOtlpStrategy: unknown targetEntity").

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php`:
- Around line 60-65: The debug statement in
DefaultEntityManyToManyCollectionDeleteAuditLogFormatter::format currently logs
the entire $deletedIds array which can be huge; change the Log::debug call to
avoid serializing the full array and instead log only the count (use
count($deletedIds) or 0 when null) and a short flag indicating null/empty,
referencing the $deletedIds variable so the formatter still provides useful
diagnostic info without dumping the full collection.

In
`@app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php`:
- Around line 55-56: Standardize the change_set keys by treating "removed_ids"
as the canonical name and explicitly map any legacy "deleted_ids" to it before
use in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter: in the code
that prepares $change_set (or immediately before computing
$addedIds/$removedIds) normalize $change_set['removed_ids'] =
$change_set['removed_ids'] ?? $change_set['deleted_ids'] ?? null, then set
$addedIds = $change_set['added_ids'] ?? null and $removedIds =
$change_set['removed_ids']; also update any docblocks/comments or
function/method docs that reference $change_set to state that "removed_ids" is
the standard key (and "deleted_ids" is supported as an alias).

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php`:
- Around line 118-123: Remove the extraneous blank lines between the assignment
of $removed_ids and the subsequent assignments to $field and $targetEntity in
the SummitAttendeeAuditLogFormatter class so the code reads compactly as
consecutive assignments (ensure $removed_ids = $collectionData['removed_ids'] ??
[]; is immediately followed by $field = $collectionData['field'] ?? 'unknown';
and $targetEntity = $collectionData['target_entity'] ?? 'unknown';).
- Around line 59-76: The method handleManyToManyCollection has an unused
$subject parameter; remove $subject from its signature (change function
handleManyToManyCollection(SummitAttendee $subject, array $change_set, $id,
$name, bool $isDeletion): ?string to handleManyToManyCollection(array
$change_set, $id, $name, bool $isDeletion): ?string) and update all call sites
that pass the subject (the two places invoking handleManyToManyCollection) to
stop providing the extra argument so they pass only $change_set, $id, $name,
$isDeletion; ensure type hints and usages inside handleManyToManyCollection
remain consistent after removal.

In `@tests/OpenTelemetry/Formatters/AuditEventListenerTest.php`:
- Around line 66-122: Add a new unit test that exercises
AuditEventListener::auditCollection()'s many-to-many delete branch by
constructing an initialized PersistentCollection (use
PersistentCollectionTestHelper to create a collection that is initialized and
has removed elements), attach it to a mock owner and mapping for a
ManyToManyOwningSideMapping, inject a mocked EntityManager (and ClassMetadata)
into the AuditEventListener instance, then call auditCollection() and assert the
emitted payload includes the diff-style "deleted_ids" (e.g., [10,11]) produced
by AuditEventListener::fetchDeletedIdsFromInitializedCollection(); ensure the
test uses the same mocking pattern as existing tests (QueryBuilder/Connection
mocks if needed) and targets the listener-level behavior rather than the helper
method directly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 866ce53b-deac-415c-b5d9-86b32cd300b4

📥 Commits

Reviewing files that changed from the base of the PR and between 257cc38 and 1e069fd.

📒 Files selected for processing (15)
  • app/Audit/AbstractAuditLogFormatter.php
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogFormatterFactory.php
  • app/Audit/AuditLogOtlpStrategy.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php
  • app/Audit/Interfaces/IAuditStrategy.php
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
  • tests/SummitRSVPServiceTest.php

}

//$ui = app()->bound('ui.context') ? app('ui.context') : [];
$ui = [];
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if $ui was previously populated from somewhere
rg -n -B5 -A5 '\$ui\s*=' app/Audit/AuditEventListener.php

# Search for any context that might provide app/flow values
rg -n "uiApp|uiFlow|ui_app|ui_flow" --type php -g '!tests/*'

Repository: OpenStackweb/summit-api

Length of output: 1022


🏁 Script executed:

cat -n app/Audit/AuditEventListener.php | sed -n '110,140p'

Repository: OpenStackweb/summit-api

Length of output: 1518


Remove unused $ui initialization or populate the array.

The $ui variable is initialized as an empty array at line 114, but it's never populated. Lines 134-135 attempt to access $ui['app'] and $ui['flow'], which will always be null due to the null coalescing operator. Either remove the initialization and use null directly, or add the logic to populate these array keys from the request/route context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditEventListener.php` at line 114, The $ui array is initialized
but never populated, making later accesses to $ui['app'] and $ui['flow'] always
null; update the AuditEventListener code to either remove the unused $ui
initialization and use null/defaults directly where $ui['app']/$ui['flow'] are
read, or populate $ui from the current request/route context (e.g. pull values
from the Request or route attributes) before those reads so $ui['app'] and
$ui['flow'] contain the intended values; ensure you update the same method in
AuditEventListener where $ui is declared and where $ui['app']/$ui['flow'] are
accessed.

Comment on lines +216 to +285
private function fetchManyToManyIds(PersistentCollection $collection, $uow): ?array
{
try {

$mapping = $collection->getMapping();

$joinTable = $mapping->joinTable ?? null;
$joinTableName = is_array($joinTable) ? ($joinTable['name'] ?? null) : ($joinTable->name ?? null);
$joinColumns = is_array($joinTable) ? ($joinTable['joinColumns'] ?? []) : ($joinTable->joinColumns ?? []);
$inverseJoinColumns = is_array($joinTable) ? ($joinTable['inverseJoinColumns'] ?? []) : ($joinTable->inverseJoinColumns ?? []);

$joinColumn = $joinColumns[0] ?? null;
$inverseJoinColumn = $inverseJoinColumns[0] ?? null;
$joinColumnName = is_object($joinColumn) ? ($joinColumn->name ?? null) : ($joinColumn['name'] ?? null);
$inverseColumnName = is_object($inverseJoinColumn) ? ($inverseJoinColumn->name ?? null) : ($inverseJoinColumn['name'] ?? null);
if (!$joinTableName) {
Log::debug("AuditEventListener::fetchManyToManyIds - no join table name found");
return null;
}

$owner = $collection->getOwner();
if ($owner === null) {
return null;
}

$ownerMeta = $this->em->getClassMetadata(get_class($owner));
$ownerIds = $ownerMeta->getIdentifierValues($owner);
if (empty($ownerIds)) {
Log::debug("AuditEventListener::fetchManyToManyIds - owner IDs are empty");
return null;
}
$ownerId = reset($ownerIds);

if (empty($joinColumns) || empty($inverseJoinColumns)) {
Log::debug("AuditEventListener::fetchManyToManyIds - join or inverse columns are empty");
return null;
}

Log::debug("AuditEventListener::fetchManyToManyIds - column names", [
'joinColumnName' => $joinColumnName,
'inverseColumnName' => $inverseColumnName
]);

if (!$joinColumnName || !$inverseColumnName) {
Log::debug("AuditEventListener::fetchManyToManyIds - column names are missing");
return null;
}

$conn = $this->em->getConnection();
$qb = $conn->createQueryBuilder();

$qb->select($inverseColumnName)
->from($joinTableName)
->where($joinColumnName . ' = :ownerId')
->setParameter('ownerId', $ownerId);

$ids = $qb->fetchFirstColumn();

$result = !empty($ids) ? array_map('intval', $ids) : [];

return $result;

} catch (\Exception $e) {
Log::error("AuditEventListener::fetchManyToManyIds error: " . $e->getMessage(), [
'exception' => get_class($e),
'trace' => $e->getTraceAsString()
]);
return null;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove unused $uow parameter.

Static analysis correctly identifies that the $uow parameter is never used in fetchManyToManyIds(). The method uses $this->em for all EntityManager operations.

🐛 Proposed fix
-    private function fetchManyToManyIds(PersistentCollection $collection, $uow): ?array
+    private function fetchManyToManyIds(PersistentCollection $collection): ?array
     {

Also update the call sites at lines 196 and 294:

-                $deletedIds = $this->fetchManyToManyIds($subject, $uow);
+                $deletedIds = $this->fetchManyToManyIds($subject);
-            $allIds = $this->fetchManyToManyIds($collection, $uow);
+            $allIds = $this->fetchManyToManyIds($collection);
🧰 Tools
🪛 PHPMD (2.15.0)

[warning] 216-216: Avoid unused parameters such as '$uow'. (undefined)

(UnusedFormalParameter)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditEventListener.php` around lines 216 - 285, Remove the unused
$uow parameter from the fetchManyToManyIds method signature and any related
docblock, then update all call sites that pass $uow into fetchManyToManyIds to
call it without that argument; specifically edit the
AuditEventListener::fetchManyToManyIds declaration to drop $uow and modify the
internal callers within the class to call fetchManyToManyIds($collection)
instead of passing the unused $uow. Ensure nothing else relies on the removed
parameter and run static analysis/tests to confirm no remaining references.

Comment on lines +250 to +264
public function testFormatWithChildFormatterSkipsNullMessages(): void
{
$childFormatter = Mockery::mock(IChildEntityAuditLogFormatter::class);
$childFormatter
->shouldReceive('format')
->twice()
->andReturn(null, 'child-delete');

$formatterWithChild = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter($childFormatter);
$formatterWithChild->setContext(AuditContextBuilder::default()->build());

$collection = $this->makeCollection(self::SNAPSHOT_IDS_UPDATE, self::CURRENT_IDS_UPDATE);
$result = $formatterWithChild->format(new \stdClass(), ['collection' => $collection]);

$this->assertSame('|child-delete', $result);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't lock in the leading separator bug.

This expectation codifies the current |child-delete artifact when one child formatter returns null. The delete formatter tests already treat null child output as skippable, so the update contract should do the same instead of preserving an empty segment.

🛠️ Proposed fix
-        $this->assertSame('|child-delete', $result);
+        $this->assertSame('child-delete', $result);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function testFormatWithChildFormatterSkipsNullMessages(): void
{
$childFormatter = Mockery::mock(IChildEntityAuditLogFormatter::class);
$childFormatter
->shouldReceive('format')
->twice()
->andReturn(null, 'child-delete');
$formatterWithChild = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter($childFormatter);
$formatterWithChild->setContext(AuditContextBuilder::default()->build());
$collection = $this->makeCollection(self::SNAPSHOT_IDS_UPDATE, self::CURRENT_IDS_UPDATE);
$result = $formatterWithChild->format(new \stdClass(), ['collection' => $collection]);
$this->assertSame('|child-delete', $result);
public function testFormatWithChildFormatterSkipsNullMessages(): void
{
$childFormatter = Mockery::mock(IChildEntityAuditLogFormatter::class);
$childFormatter
->shouldReceive('format')
->twice()
->andReturn(null, 'child-delete');
$formatterWithChild = new DefaultEntityManyToManyCollectionUpdateAuditLogFormatter($childFormatter);
$formatterWithChild->setContext(AuditContextBuilder::default()->build());
$collection = $this->makeCollection(self::SNAPSHOT_IDS_UPDATE, self::CURRENT_IDS_UPDATE);
$result = $formatterWithChild->format(new \stdClass(), ['collection' => $collection]);
$this->assertSame('child-delete', $result);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php`
around lines 250 - 264, The current formatter preserves an empty segment when a
child formatter returns null, causing a leading '|' in
DefaultEntityManyToManyCollectionUpdateAuditLogFormatter output; update the
format method in DefaultEntityManyToManyCollectionUpdateAuditLogFormatter so it
collects child outputs from the IChildEntityAuditLogFormatter->format calls,
filters out null/empty values, and then joins the remaining pieces with the '|'
separator (only inserting separators between non-empty segments) so null child
results are skipped and no leading separator is emitted.

Copy link
Contributor

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Current changes does not adhere to the proposed ADR.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from 1e069fd to 0d6d6aa Compare March 17, 2026 18:41
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
app/Audit/AbstractAuditLogFormatter.php (1)

102-104: Minor indentation inconsistency.

Line 103 has extra indentation that doesn't match the surrounding code style.

🧹 Suggested fix
     protected function getUserInfo(): string
     {
         if (app()->runningInConsole()) {
-                return 'Worker Job';
+            return 'Worker Job';
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AbstractAuditLogFormatter.php` around lines 102 - 104, In
AbstractAuditLogFormatter (the method containing the app()->runningInConsole()
check) there is an indentation mismatch: the line with "return 'Worker Job';" is
over-indented; adjust the indentation to match the surrounding block style so
the return aligns with the if block's opening brace and other statements in the
method.
app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php (1)

114-117: Remove extraneous blank lines.

Lines 115-117 contain unnecessary whitespace that should be cleaned up for consistency.

🧹 Suggested fix
         try {
             $removed_ids = $collectionData['removed_ids'] ?? [];
-
-            
-
             $field = $collectionData['field'] ?? 'unknown';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php` around
lines 114 - 117, In SummitAttendeeAuditLogFormatter, remove the extra blank
lines after the $removed_ids = $collectionData['removed_ids'] ?? []; assignment
so the assignment sits directly adjacent to the next statement; locate the
$removed_ids variable usage in this method (and the surrounding $collectionData
reference) and collapse the stray whitespace to match the project's whitespace
style.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 190-203: The current conditional uses
!empty($change_set['deleted_ids']) which treats an explicitly-provided empty
deleted_ids as "missing" and falls back to counting $collection; change the
check to detect presence of the key (e.g., array_key_exists('deleted_ids',
$change_set) or isset($change_set['deleted_ids'])) so that an explicit empty
array is handled as a delete input; keep the existing logic that sets
$data['audit.collection_count'] = count($change_set['deleted_ids']) and the
subsequent collection-initialized branch (and calls to getCollectionChanges)
when the key exists, and only fall back to counting $collection when deleted_ids
is truly absent.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`:
- Around line 94-107: Rename the test method
testManyToManyDeleteReturnsNullWithoutRemovedIds to reflect that a non-null
formatted message is expected when removed IDs are empty (for example
testManyToManyDeleteFormatsEmptyRemovedIds); update the method name in the
SummitAttendeeAuditLogFormatterManyToManyTest class and any references so it
matches the assertion behavior where $formatter->format($attendee,
['collection'=>$collection]) is asserted with assertNotNull($result) and
assertStringContainsString(self::LOG_REMOVED_IDS_EMPTY, $result); keep the
existing calls to makeFormatter, makeCollection and the assertions unchanged.

---

Nitpick comments:
In `@app/Audit/AbstractAuditLogFormatter.php`:
- Around line 102-104: In AbstractAuditLogFormatter (the method containing the
app()->runningInConsole() check) there is an indentation mismatch: the line with
"return 'Worker Job';" is over-indented; adjust the indentation to match the
surrounding block style so the return aligns with the if block's opening brace
and other statements in the method.

In `@app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php`:
- Around line 114-117: In SummitAttendeeAuditLogFormatter, remove the extra
blank lines after the $removed_ids = $collectionData['removed_ids'] ?? [];
assignment so the assignment sits directly adjacent to the next statement;
locate the $removed_ids variable usage in this method (and the surrounding
$collectionData reference) and collapse the stray whitespace to match the
project's whitespace style.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad3fdd4c-48a1-4f00-9523-0d70be51d46d

📥 Commits

Reviewing files that changed from the base of the PR and between 1e069fd and 0d6d6aa.

📒 Files selected for processing (17)
  • app/Audit/AbstractAuditLogFormatter.php
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogFormatterFactory.php
  • app/Audit/AuditLogOtlpStrategy.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/PresentationCategoryGroupAuditLogFormatter.php
  • app/Audit/ConcreteFormatters/SummitAttendeeAuditLogFormatter.php
  • app/Audit/PersistentCollectionMetadata.php
  • config/audit_log.php
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionDeleteAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/DefaultEntityManyToManyCollectionUpdateAuditLogFormatterTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php
  • tests/SummitRSVPServiceTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/OpenTelemetry/Formatters/AllFormattersIntegrationTest.php
  • tests/OpenTelemetry/Formatters/Support/PersistentCollectionTestHelper.php

Comment on lines +190 to +203
if (!empty($change_set['deleted_ids'])) {
$data['audit.collection_count'] = count($change_set['deleted_ids']);
if ($collection->isInitialized()) {
$changes = $this->getCollectionChanges($collection, $change_set);
$data['audit.collection_current_count'] = $changes['current_count'];
$data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
$data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
} else {
$data['audit.collection_current_count'] = 0;
$data['audit.collection_snapshot_count'] = 0;
$data['audit.collection_is_dirty'] = 'true';
}
} elseif ($collection->isInitialized()) {
$data['audit.collection_count'] = count($collection);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Treat deleted_ids as explicit input even when empty.

Using !empty(...) conflates “missing” with “present but empty”. For delete events, that can produce inaccurate audit.collection_count by falling back to count($collection).

🐛 Proposed fix
-                    if (!empty($change_set['deleted_ids'])) {
-                        $data['audit.collection_count'] = count($change_set['deleted_ids']);
+                    if (array_key_exists('deleted_ids', $change_set) && is_array($change_set['deleted_ids'])) {
+                        $data['audit.collection_count'] = count($change_set['deleted_ids']);
                         if ($collection->isInitialized()) {
                             $changes = $this->getCollectionChanges($collection, $change_set);
                             $data['audit.collection_current_count'] = $changes['current_count'];
                             $data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
                             $data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
                         } else {
                             $data['audit.collection_current_count'] = 0;
                             $data['audit.collection_snapshot_count'] = 0;
                             $data['audit.collection_is_dirty'] = 'true';
                         }
                     } elseif ($collection->isInitialized()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 190 - 203, The current
conditional uses !empty($change_set['deleted_ids']) which treats an
explicitly-provided empty deleted_ids as "missing" and falls back to counting
$collection; change the check to detect presence of the key (e.g.,
array_key_exists('deleted_ids', $change_set) or
isset($change_set['deleted_ids'])) so that an explicit empty array is handled as
a delete input; keep the existing logic that sets
$data['audit.collection_count'] = count($change_set['deleted_ids']) and the
subsequent collection-initialized branch (and calls to getCollectionChanges)
when the key exists, and only fall back to counting $collection when deleted_ids
is truly absent.

@andrestejerina97 andrestejerina97 force-pushed the feature/new-event-for-formatters branch from 0d6d6aa to 2311779 Compare March 17, 2026 19:07
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php (1)

71-79: ⚠️ Potential issue | 🟡 Minor

“Without context” rows still don’t hit the no-context branch.

On Lines 71-79, all provider cases pass [], so provider rows on Lines 206-207 effectively test missing collection (same as Lines 208-209) instead of “collection present but context missing”.

🧪 Proposed test fix
     public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
         string $eventType,
-        bool $withContext
+        bool $withContext,
+        bool $withCollection
     ): void {
         $formatter = $this->makeFormatter($eventType, $withContext);
         $attendee = $this->makeAttendee();
-
-        $result = $formatter->format($attendee, []);
+        $changeSet = $withCollection
+            ? ['collection' => $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_EMPTY)]
+            : [];
+        $result = $formatter->format($attendee, $changeSet);
         $this->assertNull($result);
     }
@@
-            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false],
-            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false],
-            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true],
-            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true],
+            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false, true],
+            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false, true],
+            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true, false],
+            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true, false],
         ];
     }

Also applies to: 203-210

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`
around lines 71 - 79, The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection is passing an empty
array for the formatter context for all provider cases, so it never exercises
the "collection present but context missing" branch; update the data provider
used by makeFormatter to supply two separate cases: one where the second
argument is an empty array (both collection and context missing) and one where a
collection key (matching what SummitAttendeeAuditLogFormatter expects) is
present but the required context is omitted (e.g., pass ['collection' =>
$someCollection] or the formatter's actual collection key) so the
formatter->format($attendee, $context) call tests the "collection present but
context missing" path as well; adjust the provider rows that feed
testManyToManyReturnsNullWithoutRequiredContextOrCollection and any other
affected tests (e.g., the provider used by the test at lines noted) to include
these distinct cases.
app/Audit/AuditLogOtlpStrategy.php (1)

190-203: ⚠️ Potential issue | 🟡 Minor

Handle explicitly provided empty deleted_ids distinctly from “missing”.

On Line 190, !empty($change_set['deleted_ids']) treats [] as absent and falls back to counting the collection, which can misreport delete audit counts. Use key-existence + array validation instead.

🐛 Proposed fix
-                    if (!empty($change_set['deleted_ids'])) {
+                    if (array_key_exists('deleted_ids', $change_set) && is_array($change_set['deleted_ids'])) {
                         $data['audit.collection_count'] = count($change_set['deleted_ids']);
                         if ($collection->isInitialized()) {
                             $changes = $this->getCollectionChanges($collection, $change_set);
                             $data['audit.collection_current_count'] = $changes['current_count'];
                             $data['audit.collection_snapshot_count'] = $changes['snapshot_count'];
                             $data['audit.collection_is_dirty'] = $changes['is_dirty'] ? 'true' : 'false';
                         } else {
                             $data['audit.collection_current_count'] = 0;
                             $data['audit.collection_snapshot_count'] = 0;
                             $data['audit.collection_is_dirty'] = 'true';
                         }
                     } elseif ($collection->isInitialized()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Audit/AuditLogOtlpStrategy.php` around lines 190 - 203, Replace the
!empty($change_set['deleted_ids']) check so an explicitly provided empty array
is treated as "present" instead of missing: use array_key_exists('deleted_ids',
$change_set) && is_array($change_set['deleted_ids']) to detect the key, then set
$data['audit.collection_count'] = count($change_set['deleted_ids']) (which will
be 0 for an empty array) and keep the subsequent logic that calls
getCollectionChanges($collection, $change_set) when
$collection->isInitialized(), otherwise set the current/snapshot counts and
is_dirty as before; update the condition that falls back to counting the
collection to only run when the deleted_ids key is absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php`:
- Around line 64-72: The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection currently always
passes an empty change set to $formatter->format($group, []), so cases meant to
exercise "collection present but context missing" never run; change the call to
pass a change set conditionally: when $withContext is false provide a change set
that includes a collection key (e.g. ['collection' => /* use existing factory
like $this->makeCollection() or a minimal array representing the collection */])
so the branch where collection exists but context is missing is exercised, and
make the analogous change in the other test block referenced (the similar case
around lines 147-154 / the corresponding test method) to mirror this conditional
change-set behavior.

---

Duplicate comments:
In `@app/Audit/AuditLogOtlpStrategy.php`:
- Around line 190-203: Replace the !empty($change_set['deleted_ids']) check so
an explicitly provided empty array is treated as "present" instead of missing:
use array_key_exists('deleted_ids', $change_set) &&
is_array($change_set['deleted_ids']) to detect the key, then set
$data['audit.collection_count'] = count($change_set['deleted_ids']) (which will
be 0 for an empty array) and keep the subsequent logic that calls
getCollectionChanges($collection, $change_set) when
$collection->isInitialized(), otherwise set the current/snapshot counts and
is_dirty as before; update the condition that falls back to counting the
collection to only run when the deleted_ids key is absent.

In
`@tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php`:
- Around line 71-79: The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection is passing an empty
array for the formatter context for all provider cases, so it never exercises
the "collection present but context missing" branch; update the data provider
used by makeFormatter to supply two separate cases: one where the second
argument is an empty array (both collection and context missing) and one where a
collection key (matching what SummitAttendeeAuditLogFormatter expects) is
present but the required context is omitted (e.g., pass ['collection' =>
$someCollection] or the formatter's actual collection key) so the
formatter->format($attendee, $context) call tests the "collection present but
context missing" path as well; adjust the provider rows that feed
testManyToManyReturnsNullWithoutRequiredContextOrCollection and any other
affected tests (e.g., the provider used by the test at lines noted) to include
these distinct cases.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ff8d1c86-5523-4f14-8075-928c2e2533b9

📥 Commits

Reviewing files that changed from the base of the PR and between 0d6d6aa and 2311779.

📒 Files selected for processing (5)
  • app/Audit/AuditEventListener.php
  • app/Audit/AuditLogOtlpStrategy.php
  • tests/OpenTelemetry/Formatters/AuditEventListenerTest.php
  • tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php
  • tests/OpenTelemetry/Formatters/SummitAttendeeAuditLogFormatterManyToManyTest.php

Comment on lines +64 to +72
public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
string $eventType,
bool $withContext
): void {
$formatter = $this->makeFormatter($eventType, $withContext);
$group = $this->makeGroup();

$result = $formatter->format($group, []);
$this->assertNull($result);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Provider currently duplicates missing-collection behavior for “without context” rows.

Line 71 always passes an empty change set, so the “without context” cases never validate the branch where collection is present but context is missing.

🧪 Proposed test fix
     public function testManyToManyReturnsNullWithoutRequiredContextOrCollection(
         string $eventType,
-        bool $withContext
+        bool $withContext,
+        bool $withCollection
     ): void {
         $formatter = $this->makeFormatter($eventType, $withContext);
         $group = $this->makeGroup();
-
-        $result = $formatter->format($group, []);
+        $changeSet = $withCollection
+            ? ['collection' => $this->makeCollection(self::SNAPSHOT_IDS_EMPTY, self::CURRENT_IDS_EMPTY)]
+            : [];
+        $result = $formatter->format($group, $changeSet);
         $this->assertNull($result);
     }
@@
-            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false],
-            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false],
-            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true],
-            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true],
+            self::DP_UPDATE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, false, true],
+            self::DP_DELETE_WITHOUT_CONTEXT => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, false, true],
+            self::DP_UPDATE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_UPDATE, true, false],
+            self::DP_DELETE_WITHOUT_COLLECTION => [IAuditStrategy::EVENT_COLLECTION_MANYTOMANY_DELETE, true, false],
         ];
     }

Also applies to: 147-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@tests/OpenTelemetry/Formatters/PresentationCategoryGroupAuditLogFormatterManyToManyTest.php`
around lines 64 - 72, The test
testManyToManyReturnsNullWithoutRequiredContextOrCollection currently always
passes an empty change set to $formatter->format($group, []), so cases meant to
exercise "collection present but context missing" never run; change the call to
pass a change set conditionally: when $withContext is false provide a change set
that includes a collection key (e.g. ['collection' => /* use existing factory
like $this->makeCollection() or a minimal array representing the collection */])
so the branch where collection exists but context is missing is exercised, and
make the analogous change in the other test block referenced (the similar case
around lines 147-154 / the corresponding test method) to mirror this conditional
change-set behavior.

Copy link
Contributor

@martinquiroga-exo martinquiroga-exo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants